<?php

/** @package codymvc */
 
/**
 * @class CodyMVC
 * @brief Reads config files, sets up the include path and passes control to a
 * front controller object.
 *
 * 
 */

// sets up and runs app - all you need to do is include this file, make a 
// CodyMVC instance, and call run().

// Flow:
// CodyMVC::require_files() requires the necessary system classes
// You can include any extra files here.
// CodyMVC::run() sets up environment and calls FrontController::execute()
// FrontController::execute() gets the Command object for the given URI from the
// CommandFactory, and calls execute() on the Command.  Default is a 'Command' object.
// Each Command is responsible for calling functions on Model objects, and then
// returning a string for the View to turn into display.

// Includes simple html file output View - extend this to get templating functionality
// via your favourite templating system (eg Smarty)

// Model type objects should NOT call methods on View, Command or FrontController 
// objects.  Their responsibility is to provide methods that allow the system to 
// update its' state, ie. manipulate database records, create/update/delete files.

// You can set other classes that extend the base classes as the FrontController, 
// CommandFactory, and View.  To do so, you have to make sure the class files are 
// either loaded before loading ChromeMVC, or put them in your include path and 
// call the files ClassName.inc so the autoloader can find them.

// Check version of PHP. 
if ( version_compare( phpversion(), '5.1' ) < 0 ) {
    echo "<h1>Unsupported PHP version " . phpversion() . "</h1>";
    echo "<p>CodyMVC does not run with PHP 4.</p>";
    exit;
}

class CodyMVC {

    /** 
     * Singleton static variable, holds the config values used by 
     * the framework for this request.
     */
    private static $config;

    /** @private
     * Returns the default config values for the application.  Running CodyMVC
     * using just the defaults will display the README file that comes with
     * the framework.
     * @return (Array) An associated array of strings, where keys are the
     *          names of each config item, and values are strings representing
     *          the default value for this config item.
     */
    private static function get_default_config() {
        return array(
            'default_command' => 'Command',
            'not_found_command' => 'Command',
            'error_template' => 'README',

            'command_dir' => dirname(__FILE__),
            'model_dir' => dirname(__FILE__),
            'template_dir' => dirname(__FILE__),
            'cody_dir' => dirname(__FILE__),

            'error_handler' => array(__CLASS__, 'handle_error'),
            'exception_handler' => array(__CLASS__, 'handle_exception'),
            
            'frontcontroller_class' => 'FrontController',
            'commandfactory_class' => 'CommandFactory',
            'view_class' => 'View'
        );
    }

    /**
     * Sets up config, includes needed files, sets error & exception handlers
     * and class autoloader function.
     * @return null
     */
    public function __construct($local_config=null) {

        self::set_config($local_config);
        self::require_files();

        // Error & Exception handling
        set_error_handler( self::get_config('error_handler'), error_reporting() );
        set_exception_handler( self::get_config('exception_handler') );

        // Class autoloading
        self::set_include_path();
        self::set_autoloading();
    }

    /**
     * Hands control over to the front controller.
     * @return null
     */
    public function run() {
        // Call the frontcontroller's execute() method
        call_user_func(
            array(
                self::get_config('frontcontroller_class'),
                'execute'
            )
        );
    }

    /**
     * Returns the value of a specified config item.
     * @param $name (String) The name of the config item to get the value for.
     * @return (String) The config value requested.  If the requested name
     *          is not found, returns an empty string.
     */
    public static function get_config($name) {
        if ( ! is_array(self::$config) ) {
            self::load_config();
        }    
        if ( ! is_string($name) || ! array_key_exists($name, self::$config) ) {
            return '';
        }
        return self::$config[$name];
    }

    /** 
     * Sets an associated array of config values.  When this function is called,
     * the config is set to the defaults overwritten by the current configuration,
     * then overwritten by the values sent in here.
     * @param $local_config (Array) An associated array of config names and
     *          values as strings.
     * @return null
     */
    public static function set_config($local_config=null) {
        $current_config = self::$config;
        if ( ! is_array($current_config) ) {
            $current_config = array();
        }
        if ( ! is_array($local_config) ) {
            $local_config = array();
        }
        $default_config = self::get_default_config();
        self::$config = array_merge($default_config, $current_config, $local_config);
    }

    /** @private
     * Adds the command and model directories to the include path.
     * @return null
     */
    private static function set_include_path() {
        set_include_path(
            get_include_path() . PATH_SEPARATOR . self::get_config('command_dir')
        );
        set_include_path(
            get_include_path() . PATH_SEPARATOR . self::get_config('model_dir')
        );    
    }

    /** @private
     * Requires necessary framework files.
     * @return null
     */
    private static function require_files() {
        // Framework includes - other files will extend these
        require_once(dirname(__FILE__).'/FrontController.inc');
        require_once(dirname(__FILE__).'/CommandFactory.inc');
        require_once(dirname(__FILE__).'/Command.inc');
        require_once(dirname(__FILE__).'/View.inc');
    }

    /**
     * The default error handler - throws an ErrorException.  Has arguments in
     * the correct order to use as an error handler function via the 
     * set_error_handler() PHP function.
     * @link http://nz.php.net/manual/en/errorexception.construct.php
     * @link http://nz.php.net/manual/en/function.set-error-handler.php
     * @param $errno (Int) The severity level of the exception. 
     * @param $errstr (String) The Exception message to throw. 
     * @param $errfile (String) The filename where the exception is thrown. 
     * @param $errline (Int) The line number where the exception is thrown. 
     * @return null
     */
    public static function handle_error($errno, $errstr, $errfile, $errline) {
        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    }
    
    /**
     * The default exception handler.  Prints out the exception message to the
     * screen using an error template.
     * The error template is set using config variable 'error_template'. 
     * Note that this may occur partway through rendering another template.
     * This function may be set as an exception handler using PHP function
     * set_exception_handler().
     * @link http://nz.php.net/manual/en/function.set-exception-handler.php
     * @param $e (Exception) The exception to handle.
     * @return null
     */    
    public static function handle_exception($e) {
        echo '<b>EXCEPTION</b><br>'.$e->getmessage().' FILE:'.$e->getfile().' LINE: '.$e->getline().'<hr>';
        $view_class = self::get_config('view_class');
        call_user_func(
            array(
                $view_class,
                'display'
            ),
            self::get_config('error_template') 
        );
    }

    /**
     * Registers autoload_class() as an autoload function for undefined
     * classes using the SPL (Standard PHP Library) if available.  Otherwise
     * loads the __autoload.inc file which wraps this function inside the 
     * standard PHP __autoload() function.
     * @return null
     */
    public static function set_autoloading() {
        if ( function_exists('spl_autoload_register') ) {
            spl_autoload_register(
                array(
                    __CLASS__,
                    'autoload_class'
                )
            );
            return;
        }
        require_once( dirname(__FILE__).'/__autoload.inc' );
    }

    /**
     * Loops through the include path, attemping to find a file with the same
     * name as the specified class with either .inc or .php extension.
     * If a file is found, calls require_once() on this file.
     * @param $class-name (String) The name of the class to attempt to autoload.
     * @return null;
     */
    public static function autoload_class($class_name) {
        $directories = explode(PATH_SEPARATOR, get_include_path());
        foreach( $directories as $directory ) {
            if ( is_file($directory.'/'.$class_name.'.inc') ) {
                require_once($directory.'/'.$class_name.'.inc');
                return;
            }
            if ( is_file($directory.'/'.$class_name.'.php') ) {
                require_once($directory.'/'.$class_name.'.php');
                return;
            }
        }
    }

}


?>
